{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import math\n",
    "import random\n",
    "from sklearn.metrics import log_loss\n",
    "\n",
    "class FFMClassifier(object):\n",
    "    \n",
    "    def __init__(self,n,m,k,eta,lambd,feat_field_dic):\n",
    "        # n个特征，\n",
    "        # m个域，\n",
    "        # 隐向量的维度k\n",
    "        # 学习率η\n",
    "        # 正则参数λ\n",
    "        # feat_field_dic 为每个特征的域索引。格式：{feature_index1:field_index1,......}\n",
    "        self.n = n\n",
    "        self.m = m\n",
    "        self.k = k\n",
    "        self.eta = eta\n",
    "        self.lambd = lambd\n",
    "        self.dic = feat_field_dic\n",
    "        # 初始化三维权重矩阵w，Xavier初始化w∼G(0,√1/k)\n",
    "        self.w = np.random.rand(n,m,k)/math.sqrt(k)\n",
    "#         self.w = np.random.rand(n,m,k)\n",
    "        # self.G是每轮梯度平方和,用于AdaGrad梯度下降\n",
    "        self.G = np.ones(shape=(n,m,k),dtype=np.float64)\n",
    "        \n",
    "    #用于计算FFM的模型表达式\n",
    "    def phi(self,each_x):\n",
    "        phi_result = 0 \n",
    "        #归一化因子\n",
    "        sum_v = sum(each_x.values)\n",
    "        # 获取特征index\n",
    "        key_index = each_x.keys()\n",
    "        #循环一条数据的每个特征\n",
    "        for i in range(len(key_index)):\n",
    "            #feat_index1为特征索引，fild_index1为特征所属域的索引，value1为特征值\n",
    "            feat_index1 = i\n",
    "            fild_index1 = self.dic[feat_index1]\n",
    "            value1 = each_x[feat_index1]/sum_v \n",
    "            #计算每个特征与其他特征的组合\n",
    "            for j in range(i+1,len(key_index)):\n",
    "                feat_index2 = j\n",
    "                fild_index2 = self.dic[feat_index2]\n",
    "                value2 = each_x[feat_index2]/sum_v \n",
    "                w1=self.w[feat_index1,fild_index2]\n",
    "                w2=self.w[feat_index2,fild_index1]\n",
    "                #模型公式:∑∑<Wi.fj,Wj.fi>XiXj\n",
    "                phi_result += np.dot(w1, w2) * value1 * value2\n",
    "        return phi_result\n",
    "    \n",
    "    #用于随机梯度下降优化更新参数\n",
    "    def sgd_para(self,each_x,grad_phi):\n",
    "        #grad_phi为目标函数对φ(x)的导数。grad_phi与w无关，故每条数据提前计算好\n",
    "        sum_v = sum(each_x.values)\n",
    "        key_index = each_x.keys()\n",
    "        for i in range(len(key_index)):\n",
    "            feat_index1 = i\n",
    "            fild_index1 = self.dic[feat_index1]\n",
    "            value1 = each_x[feat_index1]/sum_v\n",
    "            for j in range(i+1,len(key_index)):\n",
    "                feat_index2 = j\n",
    "                fild_index2 = self.dic[feat_index2]\n",
    "                value2 = each_x[feat_index2]/sum_v\n",
    "                w1=self.w[feat_index1,fild_index2]\n",
    "                w2=self.w[feat_index2,fild_index1]\n",
    "                #目标函数对Wi,fj，Wj,fi的偏导\n",
    "                g_i_fj = grad_phi*value1*value2*w2 + self.lambd*w1\n",
    "                g_j_fi = grad_phi*value1*value2*w1 + self.lambd*w2\n",
    "                #各个维度上的梯度累积平方和\n",
    "                self.G[feat_index1, fild_index2] += g_i_fj ** 2\n",
    "                self.G[feat_index2, fild_index1] += g_j_fi ** 2\n",
    "                # AdaGrad，更新两个w\n",
    "                self.w[feat_index1, fild_index2] -= g_i_fj*self.eta/np.sqrt(self.G[feat_index1, fild_index2])\n",
    "                self.w[feat_index2, fild_index1] -= g_j_fi*self.eta/np.sqrt(self.G[feat_index2, fild_index1])\n",
    "                \n",
    "                \n",
    "    def train(self,train_x,train_y,max_echo,min_loss):\n",
    "        # train_x 训练集x\n",
    "        # val_x 校验集x\n",
    "        # train_y 训练集y\n",
    "        # val_y 校验集y\n",
    "        # max_echo 最大迭代次数\n",
    "        # min_loss loss阈值\n",
    "        for i in range(max_echo):\n",
    "            n = 0  \n",
    "            order = list(range(len(train_y)))\n",
    "            #打乱顺序\n",
    "            random.shuffle(order)\n",
    "            #训练集训练\n",
    "            for each_train_index in order:\n",
    "                #根据index从训练集取出这条x\n",
    "                train_each_x = train_x.iloc[each_train_index]\n",
    "                #构造模型φ(x)表达式\n",
    "                phi = self.phi(train_each_x)\n",
    "                #此条数据对应的y值\n",
    "                y_i = train_y.iloc[each_train_index][0]\n",
    "                #目标函数对φ(x)的导数\n",
    "                g_phi = -y_i/(1 + math.exp(y_i * phi))\n",
    "                #梯度下降，更新参数\n",
    "                self.sgd_para(train_each_x,g_phi)\n",
    "                if n%500 == 0:\n",
    "                    print(\"epoch: {},第: {} 条数据训练\".format(i,n))\n",
    "                n = n+1\n",
    "            y_pred = self.predict(train_x)\n",
    "            loss_train = log_loss(train_y,y_pred)\n",
    "            print(\"epoch: {}, train_loss: {}\".format(i,loss_train))\n",
    "            if loss_train<= min_loss:\n",
    "                print('损失已小于阈值！训练提前结束!')\n",
    "                break\n",
    "                \n",
    "                \n",
    "    def predict_proba(self,test_x):\n",
    "        test_y =np.zeros((test_x.shape[0],2))\n",
    "        for index,row in test_x.iterrows():\n",
    "            phi_y = self.phi(row)\n",
    "            proba = 1.0/(1.0 + math.exp(-phi_y))\n",
    "            test_y[index,0] = 1 - proba\n",
    "            test_y[index,1] = proba\n",
    "            \n",
    "        return test_y\n",
    "    \n",
    "    def predict(self,test_x):\n",
    "        test_y =np.zeros(test_x.shape[0])\n",
    "        for index,row in test_x.iterrows():\n",
    "            phi_y = self.phi(row)\n",
    "            test_y[index] = phi_y\n",
    "            \n",
    "        return test_y\n",
    "        \n",
    "\n",
    "    \n",
    "    \n",
    "    #训练好的权重矩阵保存           \n",
    "    def save_model(self,file_name):\n",
    "        np.save(file_name,self.w)\n",
    "    \n",
    "    \n",
    "    #加载权重矩阵\n",
    "    def load_model(self,file_name):\n",
    "        w = np.load(file_name)\n",
    "        self.w = w\n",
    "        \n",
    "        \n",
    "        "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
